Skip to content

Context Engineering ​

标签
AI/agent/Context
字数
4383 字
阅读时间
17 分钟

管模型看到什么的一层。和 Prompt Engineering 管的「这句话怎么措辞」不同,这一层管的是一次调用里模型看到的全部输入怎么组装。

这一篇讲概念与背后的经济账;具体流水线(GSSC、NoteTool、TerminalTool)在 19-上下文工程的工程实践。

术语来源 ​

时间事件
2025-06-18Shopify CEO Tobi Lütke 在推文中提出:「为任务提供所有上下文,使其对 LLM 来说合理可解的艺术」
一周后Andrej Karpathy 公开背书,把定义收窄成「用恰到好处的信息填充上下文窗口以完成下一步,是一门精妙的艺术与科学」
其后Anthropic 给出正式表述:在推理过程中策展和维护最优 token 集合的策略

心智模型:上下文窗口是 RAM ​

Karpathy 给过一组类比:把 LLM 当成 CPU,上下文窗口就是 RAM,应用代码扮演操作系统的角色——每次调用前把正确的数据加载进这块有限的工作内存。

这组类比解释了为什么这一层的工作量落在应用侧而不是模型侧:模型能力是固定的,能让下一步「可解」的只有喂进去的东西。

一组类比:把 LLM 当成计算机。

   ┌──────────────────┐
   │ LLM              │  ← CPU:能力固定,这一层改不了
   └────────┬─────────┘
            │
   ┌────────▼─────────┐
   │ 上下文窗口        │  ← RAM:有限的工作内存
   └────────┬─────────┘
            │
   ┌────────▼─────────┐
   │ 应用代码          │  ← 操作系统:每次调用前把正确的数据加载进来
   └──────────────────┘

这组类比解释了为什么这一层的工作量落在应用侧而不是模型侧:
   模型能力是固定的,能让下一步「可解」的只有喂进去的东西。

四个核心操作
   选择    哪些内容进窗口
   压缩    砍到只留有信息的 token
   排序    放到模型注意力更高的位置
   裁剪    移除已经失效的内容

四个核心操作 ​

操作做什么
选择哪些内容进窗口
压缩砍到只留有信息的 token
排序放到模型注意力更高的位置
裁剪移除已经失效的内容

六个信息源抢同一份预算 ​

每次调用时,下面六类内容争夺同一个 token 上限:

每次调用时,六类内容争夺同一个 token 上限

   ┌───────────────────────────────────────────────┐
   │ 系统指令       系统提示与角色定义                │
   │ 任务状态       当前进度、计划、未完成项           │
   │ 检索文档       RAG 召回的内容                    │
   │ 长期记忆       跨会话留存的反思与事实             │
   │ 工具定义       工具清单与 schema                 │
   │ 工具返回结果    本轮的 Observation               │
   └───────────────────────┬───────────────────────┘
                           ▼
                    同一个 token 上限

   工程目标是找到「能让下一步可解的最小集合」,
   而不是把能塞进去的都塞进去。

   └─ 六类里「工具定义」最容易被低估:
      它是每一轮都重复发送的固定开销,而且随工具数量线性增长。
      装超过 100 个工具时,预加载会给初始提示词带来约 5–7 万 token,
      而且每一轮都重发一次(下一节有 Uber 的实测数字)。

工程目标是找到能让下一步可解的最小集合,而不是把能塞进去的都塞进去。

这六类里「工具定义」那一项最容易被低估——它是每一轮都重复发送的固定开销,而且随工具数量线性增长。下面有 Uber 的实测数字。

Prompt Caching:这一层最直接的省钱手段 ​

如果每一轮都重新传输完整的对话历史,就要为同一段前缀反复支付全额输入费用。Prompt Caching 就是针对这件事的。

机制 ​

Anthropic 于 2024-12-05 正式引入。工作方式很朴素:检查提示前缀是否已被缓存——命中就用缓存版本,未命中就处理完整提示并把前缀缓存起来。

关键是在提示里把可缓存的部分标出来。做法是把静态内容(系统指令、上下文、工具定义)放在开头,并用 cache_control 参数标记可缓存内容的结束位置。最多可以定义 4 个缓存断点,允许分别缓存不同的可重用部分。

价格结构(这是决策依据) ​

操作相对基准价格
写入 5 分钟缓存1.25 倍
写入 1 小时缓存2 倍
从缓存读取0.1 倍

读便宜、写贵。 所以缓存不是免费的优化,它是一个需要算的取舍:缓存要被复用多少次,才能摊掉写入溢价。

三个硬约束 ​

一、有最小 token 阈值。 Sonnet 和 Haiku 是 1024 token,Opus 可达 2048–4096。低于阈值,存储与查找缓存的开销不值得。

二、缓存是按模型隔离的。 Opus / Sonnet / Haiku 有不同的架构与权重,从相同文本算出的 KV 缓存不可互换。在 Opus 里建了长上下文再切到 Sonnet,Sonnet 无法重用那份缓存。

所以「切换模型省成本」这个直觉可能是错的——从表面单价看省了,但原来能以缓存读取价(0.1×)读到的上下文,现在可能需要重新写入并重算(1.25×–2×)。

三、缓存会被这些操作失效:改动 tool_choice.type、提示中任何位置图片的出现或缺失发生变化。缓存前缀必须是逐字节相同的前缀。

缓存的价格是「读便宜、写贵」,所以它是一个要算的取舍。

   写入缓存(5 分钟 TTL)   ███        1.25×
   写入缓存(1 小时 TTL)   █████      2×
   从缓存读取               ▏          0.1×

   └─ 相对基准输入价(1×)的倍数
   └─ 关键是算:缓存要被复用多少次,才能摊掉写入溢价

三个硬约束
   ① 有最小 token 阈值
        Sonnet / Haiku 是 1024 token,Opus 可达 2048–4096
        └─ 低于阈值时,存储与查找缓存的开销不值得
   ② 缓存按模型隔离
        Opus / Sonnet / Haiku 架构与权重不同,
        从相同文本算出的 KV 缓存不可互换
        └─ 在 Opus 里建了长上下文再切到 Sonnet,Sonnet 无法重用那份缓存
        └─ 所以「切换模型省成本」这个直觉可能是错的:
           表面单价省了,但原来能以 0.1× 读到的上下文,
           现在可能需要重新写入并重算(1.25×–2×)
   ③ 这些操作会让缓存失效
        改动 tool_choice.type
        提示中任何位置图片的出现或缺失发生变化
        └─ 缓存前缀必须是逐字节相同的前缀

机制:把静态内容(系统指令、上下文、工具定义)放在开头,
用 cache_control 标记可缓存内容的结束位置 —— 最多可定义 4 个缓存断点。

监控什么:API 响应里 usage 的「缓存写入 token 数」与「缓存读取 token 数」,
这两个数能直接算出缓存有没有真的被复用。

TTL 怎么选 ​

TTL 有两种:5 分钟和 1 小时。每次使用缓存内容都会刷新计时器。

选哪个取决于对话轮次之间的间隔:

场景选择理由
交互式会话,人经常让会话空闲超过 5 分钟1 小时空闲会让前缀缓存失效,下次不得不重建完整上下文——写贵点也比反复重建便宜
子智能体,只执行单一、短生命周期的任务5 分钟交互频繁、缓存几乎从不过期,用更低的写入溢价

这两条是 Uber 在公开的实践里给出的:他们因为「工程师经常让交互式会话空闲超过 5 分钟」而从默认的 5 分钟 TTL 过渡到 1 小时,同时给子智能体保留 5 分钟。

监控什么 ​

看 API 响应里 usage 的缓存写入 token 数与缓存读取 token 数。这两个数能直接算出缓存有没有真的被复用。

Agentic workload 的成本结构 ​

理解这一层为什么重要,先看 agent 的负载长什么样:

Agent 工作负载 = 执行循环 + 状态常驻

它的开销自然分成三块(AgentServe 等系统研究也指出这一点):

阶段内容资源特征
Cold prefill长 system prompt / 工具定义 / repo 指引的首次编码高 compute + 高 HBM 带宽
Resume prefill把工具输出追加到已缓存上下文后继续强依赖 cache 命中与 KV 复用
Short decode每轮生成少量 token,但轮数多对尾延迟敏感,往往更 memory-bound

这个分解解释了两件事:

  • 为什么缓存命中率是 agent 性能的一等指标——Resume prefill 是每一轮都发生的事,它直接吃缓存命中率
  • 为什么「每轮只生成几个 token」不代表轻量——Short decode 的瓶颈在轮数与尾延迟,不在单轮输出量

还有一条反直觉的:CPU 不是配角。Intel / Georgia Tech 对多种 agentic workload 做 profile,发现CPU 上的工具处理可占总延迟最高 90.6%。也就是说,串行化的瓶颈常常不在 GPU 推理,而在工具执行那一侧。

agent 的负载 = 执行循环 + 状态常驻,开销自然分三块。

  ┌─────────────────────────────────────────────────────────────┐
  │ Cold prefill     长 system prompt / 工具定义 / repo 指引的     │
  │                  首次编码                                    │
  │                  资源特征:高 compute + 高 HBM 带宽            │
  └──────────────────────────┬──────────────────────────────────┘
                             ▼
  ┌─────────────────────────────────────────────────────────────┐
  │ Resume prefill   把工具输出追加到已缓存上下文后继续              │
  │                  资源特征:强依赖 cache 命中与 KV 复用          │
  │                  └─ 每一轮都发生                              │
  └──────────────────────────┬──────────────────────────────────┘
                             ▼
  ┌─────────────────────────────────────────────────────────────┐
  │ Short decode     每轮生成少量 token,但轮数多                   │
  │                  资源特征:对尾延迟敏感,往往更 memory-bound     │
  └─────────────────────────────────────────────────────────────┘

这个分解解释了两件事
   ① 为什么缓存命中率是 agent 性能的一等指标 ——
      Resume prefill 每轮都发生,它直接吃缓存命中率
   ② 为什么「每轮只生成几个 token」不代表轻量 ——
      Short decode 的瓶颈在轮数与尾延迟,不在单轮输出量

还有一条反直觉的:CPU 不是配角
   Intel / Georgia Tech 对多种 agentic workload 做 profile,
   发现 CPU 上的工具处理可占总延迟最高 90.6%。
   └─ 串行化的瓶颈常常不在 GPU 推理,而在工具执行那一侧

工具定义:最容易被忽略的固定开销 ​

上一节说「工具定义」是六类信息源里最值得警惕的一项。Uber 给的数字很具体:

标准 MCP 会把全部工具 schema 加载到每一个会话,无论工程师在那个会话里是否会调用这些工具。装超过 100 个工具时,这种预加载会给初始提示词带来大约 5 万到 7 万 token 的开销,而且每一轮上下文交互都会重复发送这部分内容。

5–7 万 token 每轮重复——在 20 轮的会话里就是 100 万 token 级别。这已经是成本结构的主要部分,而不是「浪费一点」。

两个互补的解法:

方案做法效果
CLI 工具解析不直接集成 MCP,改为允许模型执行 Shell 命令,CLI 在调用时动态解析并调用所需工具把工具 schema 从会话上下文里彻底移除
工具检索支持数千个工具,模型检索工具目录、按需加载用到的那几个缓解膨胀;即便工具库不断扩充也能保持较高的工具选择准确率,避免工具过多导致的选择性能下降

附带收益:当工具以 Shell 命令形式暴露时,模型可以在单个脚本里批量执行多个操作(代码模式)。这是把「多轮工具调用」压成「一次调用」的机会。

这套优化的整体效果:Uber 公开的数字是智能体请求涨了 9.4 倍,token 账单没涨——他们的统一 MCP 网关覆盖 1000+ 个内部与第三方 SaaS 服务器。

这条经验对 MCP 的部署方式有直接影响:MCP 是标准,但「每个工具都预加载」不是唯一用法——工具换成 CLI 或加一层检索,才是能扩到上千工具的形状。

工具定义膨胀的量级,以及两个互补的解法。

   问题:标准 MCP 把全部工具 schema 加载到每一个会话,
        无论工程师在那个会话里是否会调用这些工具

   装超过 100 个工具时
       初始提示词 +5–7 万 token
       而且每一轮上下文交互都重复发送这部分内容
       └─ 20 轮的会话里就是 100 万 token 级别
          已经是成本结构的主要部分,而不是「浪费一点」

   ┌───────────────────────────────────────────────────────────┐
   │ 解法一:CLI 工具解析                                        │
   │   不直接集成 MCP,改为允许模型执行 Shell 命令,               │
   │   CLI 在调用时动态解析并调用所需工具                          │
   │   效果:把工具 schema 从会话上下文里彻底移除                   │
   └───────────────────────────────────────────────────────────┘
   ┌───────────────────────────────────────────────────────────┐
   │ 解法二:工具检索                                            │
   │   支持数千个工具,模型检索工具目录、按需加载用到的那几个        │
   │   效果:缓解膨胀;即便工具库不断扩充也能保持较高的工具选择准确率,
   │         避免工具过多导致的选择性能下降                        │
   └───────────────────────────────────────────────────────────┘

   附带收益:工具以 Shell 命令形式暴露时,模型可以在单个脚本里
   批量执行多个操作(代码模式)—— 把「多轮工具调用」压成「一次调用」。

整体效果:Uber 公开的数字是「智能体请求涨了 9.4 倍,token 账单没涨」,
他们的统一 MCP 网关覆盖 1000+ 个内部与第三方 SaaS 服务器。
   └─ 对 MCP 部署方式的直接影响:MCP 是标准,
      但「每个工具都预加载」不是唯一用法。

长时程任务的两个基础手法 ​

上下文一满就得处理,两个反复出现的技术:

Compaction(压缩重启)。 上下文临近窗口上限时,把已有内容总结成更短的表示,再以这个摘要为起点重新开始。这是用信息损失换「继续往下跑」的能力。

结构化笔记。 把中间结果写进外部存储(文件、笔记),需要时再注入,不常驻窗口。和 compaction 的分工是:compaction 丢细节换空间,结构化笔记保留细节但延迟加载。

完整的长时程三手段(加上子代理架构)与各自的适用面见 19-上下文工程的工程实践。

失效模式:context rot ​

无关或过量的上下文堆积之后,模型表现下降。这是这一层独有的失效信号——提示词写得再好也救不回一个塞满错误文档的窗口。

可观测的信号是:输出开始答非所问、忽略明确给出的指令、把不相关的内容当成依据。出现这些时,先怀疑输入组装,而不是继续改措辞。

还有一个相关的现象是 lost in the middle:模型对长上下文中间部分的注意力低于两端。这条直接推出一个装配规则——最强的块要放最前和最后(见 11-RAG 检索增强 的上下文组装一节)。

这一层在体系里的位置 ​

它在 Prompt Engineering → Context Engineering → Harness Engineering 这条线中间:比措辞更靠上(管的是输入整体,不是单句),比 Harness 更靠下(管不了工具、权限、验证这些跨轮次生效的东西)。

三层的完整对比与诊断判据见 Harness Engineering。

相关 ​

参考 ​

贡献者 ​

文件历史 ​